fix(grafana): discover App Platform API version instead of hardcoding v1 - #221
Conversation
The nine App Platform-backed actions (dashboard/folder get, create, update,
delete and list_folders) hardcoded `v1` in the API group path. That version
only exists in Grafana 13.0+, so on every Grafana 12.x release the requests
returned a Kubernetes-style 404 ("the server could not find the requested
resource").
Discover the served version via `GET /apis/<group>` instead, picking the first
match from a preference list and caching the result per baseUrl and group.
`v1` stays first so Grafana 13+ keeps using the GA version. Only the v1 lineage
is considered: the v2 lineage uses a different dashboard resource schema and is
not interchangeable with what normalizeDashboard() expects. If discovery fails
the previous `v1` behaviour is kept.
Summary by CodeRabbit
WalkthroughGrafana App Platform paths now resolve API versions dynamically through Sequence Diagram(s)sequenceDiagram
participant GrafanaRuntime
participant GrafanaAPI
participant apiVersionCache
GrafanaRuntime->>apiVersionCache: Check cached group version
apiVersionCache-->>GrafanaRuntime: Return cached version when available
GrafanaRuntime->>GrafanaAPI: Discover versions at /apis/{group}
GrafanaAPI-->>GrafanaRuntime: Return served versions
GrafanaRuntime->>apiVersionCache: Store selected version
GrafanaRuntime->>GrafanaAPI: Execute folder or dashboard request
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✨ Finishing Touches✨ Simplify code
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/providers/grafana/runtime.ts`:
- Line 476: The grafanaApiVersionCache update currently stores the fallback
version even when discovery fails. In the Grafana API version resolution flow,
only call grafanaApiVersionCache.set after a compatible version is successfully
discovered; leave the cache unchanged on errors or fallback results so later
requests can retry discovery.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 759259eb-f5dc-40b8-aab8-3d6652e5ad65
📒 Files selected for processing (1)
src/providers/grafana/runtime.ts
There was a problem hiding this comment.
Pull request overview
This PR fixes Grafana App Platform (Kubernetes-style) dashboard/folder actions against Grafana 12.x by discovering the served API group version at runtime (instead of hardcoding v1), and caching the resolved version per baseUrl + group to avoid repeated discovery requests.
Changes:
- Add App Platform API group/version discovery (
GET /apis/<group>) with a preferred-version list (v1,v1beta1,v0alpha1) and a per-connection cache. - Convert
apiPath()to async and update the nine dashboard/folder action call sites toawaitthe resolved API path.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| let resolved: string = grafanaApiVersionPreference[0]; | ||
| try { | ||
| const payload = await grafanaRequestJson(`/apis/${group}`, { method: "GET" }, context); | ||
| const record = optionalRecord(payload) ?? {}; | ||
| const served = new Set( | ||
| objectArrayOrEmpty(record.versions) | ||
| .map((entry) => optionalString(entry.version)) | ||
| .filter((version): version is string => version !== undefined), | ||
| ); | ||
| const match = grafanaApiVersionPreference.find((version) => served.has(version)); | ||
| if (match !== undefined) { | ||
| resolved = match; | ||
| } | ||
| } catch { | ||
| // Discovery is best-effort. Falling back to the newest known version keeps the | ||
| // previous behaviour for servers that do not expose the discovery endpoint. | ||
| } | ||
|
|
||
| grafanaApiVersionCache.set(cacheKey, resolved); | ||
| return resolved; |
| // the v2 lineage uses a different resource schema and is not interchangeable here. | ||
| const grafanaApiVersionPreference = ["v1", "v1beta1", "v0alpha1"] as const; | ||
|
|
||
| const grafanaApiVersionCache = new Map<string, string>(); |
| async function resolveGrafanaApiVersion( | ||
| group: string, | ||
| context: GrafanaContext & { phase: GrafanaRequestPhase }, | ||
| ): Promise<string> { | ||
| const cacheKey = `${context.baseUrl}|${group}`; | ||
| const cached = grafanaApiVersionCache.get(cacheKey); | ||
| if (cached !== undefined) { | ||
| return cached; | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
src/providers/grafana/runtime.test.ts (1)
25-54: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winCover cache-key isolation, not only cache hits.
This test only reuses one
baseUrland API group. A cache accidentally keyed only by group—or globally—would still pass. Add a second base URL with a different discovered version, and a second API group if available, to verify the${baseUrl}|${group}contract insrc/providers/grafana/runtime.ts.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/providers/grafana/runtime.test.ts` around lines 25 - 54, The Grafana discovery test only verifies cache reuse, not cache isolation by base URL and API group. Extend the test around grafanaActionHandlers.list_folders and its discovery mock to perform discovery against a second baseUrl with a different version, and use a second API group if the runtime exposes one, asserting separate discovery requests and version-specific URLs according to the `${baseUrl}|${group}` cache-key contract.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@src/providers/grafana/runtime.test.ts`:
- Around line 25-54: The Grafana discovery test only verifies cache reuse, not
cache isolation by base URL and API group. Extend the test around
grafanaActionHandlers.list_folders and its discovery mock to perform discovery
against a second baseUrl with a different version, and use a second API group if
the runtime exposes one, asserting separate discovery requests and
version-specific URLs according to the `${baseUrl}|${group}` cache-key contract.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 3e56012a-432e-4f73-9563-9c221aa8417e
📒 Files selected for processing (2)
src/providers/grafana/runtime.test.tssrc/providers/grafana/runtime.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- src/providers/grafana/runtime.ts
Adds four read-only alerting actions to the Grafana provider: | Action | Endpoint | |---|---| | `list_alert_rules` | `GET /api/v1/provisioning/alert-rules` | | `get_alert_rule` | `GET /api/v1/provisioning/alert-rules/:uid` | | `list_alert_instances` | `GET /api/alertmanager/grafana/api/v2/alerts` (supports `active`/`silenced`/`inhibited` filters) | | `list_contact_points` | `GET /api/v1/provisioning/contact-points` | These endpoints belong to Grafana's stable legacy REST API and are not affected by App Platform API version discovery (#221). This exact code has been running against Grafana 12.4.3 in a private deployment for two weeks. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Problem
All nine App Platform-backed Grafana actions fail against Grafana 12.x with a Kubernetes-style 404:
Affected:
get_dashboard,create_dashboard,update_dashboard,delete_dashboard,list_folders,get_folder,create_folder,update_folder,delete_folder.The six legacy-REST actions (
search_dashboardsand the five*_data_sourceactions) are unaffected, which makes this easy to misdiagnose: credential validation calls/api/org(legacy REST), so the connection reportsconfigured: trueand resolves the org profile correctly. The connection looks healthy and only fails once a dashboard/folder action is invoked.Root cause
src/providers/grafana/runtime.tshardcodes the API group version asv1:Grafana's App Platform API groups are versioned per release, and
v1only became available in Grafana 13.0. Versions actually served (GET /apis/<group>):dashboard.grafana.appfolder.grafana.appv1beta1(preferred),v0alpha1,v2alpha1v1beta1v1beta1(preferred),v0alpha1,v2beta1,v2alpha1v1beta1v1v1Requesting an unserved version returns 404 rather than negotiating down, so every Grafana below 13.0 is affected. Reproduced identically on
v1.1.0,v1.3.1andlatest.Fix
Discover the served version at runtime instead of hardcoding it:
GET /apis/<group>, then pick the first match from a preference list.baseUrl+ group, so discovery costs one extra request per connection.apiPath()becomes async and takes the request context; the nine call sitesawaitit.Three deliberate choices:
v1is preferred, so Grafana 13+ keeps using the GA version — this is not a downgrade.v2beta1/v2alpha1use a different dashboard resource schema and are not interchangeable with whatnormalizeDashboard()expects.v1, preserving current behaviour for servers that do not expose the discovery endpoint.Verification
A/B test with the same official image, the same Grafana 12.4 instance and the same service account token, differing only by the patched
runtime.tsmounted over the original:npm run fix-checkandnpm test(57 files, 563 tests) pass.Notes
Only
src/providers/grafana/runtime.tschanges — no action schemas, no catalog regeneration, no changes to the legacy-REST actions.There are no existing Grafana tests in the repo, so this change is covered by the manual A/B verification above rather than by a unit test. Happy to add one against a mocked discovery endpoint if you'd like.